Skip to content

Migrate Edge Configuration to SDK#413

Open
timmarkhuff wants to merge 21 commits intomainfrom
tim/edge-config
Open

Migrate Edge Configuration to SDK#413
timmarkhuff wants to merge 21 commits intomainfrom
tim/edge-config

Conversation

@timmarkhuff
Copy link
Contributor

@timmarkhuff timmarkhuff commented Mar 12, 2026

Moves the edge endpoint config schema from edge-endpoint into the Python SDK so users can programmatically build and validate edge configurations.

What's new

  • New groundlight.edge submodule with Pydantic models ported from edge-endpoint/app/core/configs.py:
    • EdgeEndpointConfig -- top-level config (includes global_config) with ergonomic add_detector() support
    • DetectorsConfig -- detector-scoped config model for detector-only workflows
    • InferenceConfig -- per-detector inference behavior (frozen/immutable)
    • GlobalConfig, DetectorConfig -- supporting models
  • Preset constants aligned with edge-endpoint defaults:
    • DEFAULT
    • EDGE_ANSWERS_WITH_ESCALATION
    • NO_CLOUD
    • DISABLED
  • Validation parity preserved:
    • disable_cloud_escalation requires always_return_edge_prediction=True
    • duplicate detector ID checks
    • detector config cross-reference checks (detectors[*].edge_inference_config must exist in edge_inference_configs)

Usage

from groundlight.edge import (
    DEFAULT,
    EDGE_ANSWERS_WITH_ESCALATION,
    GlobalConfig,
    NO_CLOUD,
    EdgeEndpointConfig,
    DetectorsConfig,
)

# Path 1: Build a full edge endpoint config
full_config = EdgeEndpointConfig(
    global_config=GlobalConfig(
        refresh_rate=30.0,
        confident_audit_rate=0.0,
    )
)
full_config.add_detector("det_abc123", NO_CLOUD)
full_config.add_detector("det_def456", DEFAULT)

# Path 2: Build detector-only changes
detectors_config = DetectorsConfig()
detectors_config.add_detector("det_abc123", NO_CLOUD)
detectors_config.add_detector("det_xyz789", EDGE_ANSWERS_WITH_ESCALATION)

Design decisions

  • InferenceConfig is frozen to prevent accidental mutation of shared preset objects.
  • add_detector() accepts either a detector ID string or a Detector object (consistent with existing SDK ergonomics).
  • Inference configs are auto-registered on first use via add_detector(); no separate registration step is needed.
  • EdgeEndpointConfig composes DetectorsConfig and delegates detector mutations through add_detector().
  • DetectorsConfig.to_payload() provides explicit detector-scoped wire serialization for future HTTP use.
  • Serialization matches the edge-endpoint YAML shape (detectors as a list, edge_inference_configs as a dict keyed by config name).
  • name on InferenceConfig is excluded from serialization because it is represented as the dict key.

Quality and tests

  • Added focused unit tests for edge config behavior in test/unit/test_edge_config.py, including:
    • duplicate detector and inference-config collision behavior
    • add_detector() duplicate detector ID rejection
    • constructor-time rejection of undefined edge_inference_config references
    • acceptance of value-equivalent named InferenceConfig objects
    • InferenceConfig validation constraints
    • DetectorsConfig.to_payload() payload shape
    • flattened EdgeEndpointConfig.model_dump() payload shape

Follow-up work

  • Update edge-endpoint to import these models from the Python SDK instead of defining them locally.
  • Add edge-endpoint runtime config API (proposed names):
    • POST /set-edge-config (full config replace)
    • GET /edge-config (get current active config)
    • detector-scoped endpoint(s), likely one of:
      • POST /set-edge-detectors with mode replace|merge
      • or separate add/remove detector operations
  • Add SDK methods (proposed names) that wrap those APIs:
    • set_edge_endpoint_config(config, *, timeout_s=...)
    • get_edge_endpoint_config()
    • set_edge_endpoint_detectors(detectors, *, mode="replace"|"merge", timeout_s=...)
    • optional: remove_edge_endpoint_detectors(detector_ids, *, timeout_s=...)
  • Implement retry behavior in SDK for "endpoint busy" responses (bounded exponential backoff + timeout budget).

@@ -0,0 +1,41 @@
"""Example of constructing an edge endpoint configuration programmatically."""
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is temporary, I'll get rid of this before merging.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Getting rid of it is too easy, grab the most important snippets and put them in docs in a md file, we auto push those to a webpage

Copy link
Collaborator

@brandon-wada brandon-wada left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks good so far, we should discuss more what the calls to update the config on the remote machine should look like

@@ -0,0 +1,41 @@
"""Example of constructing an edge endpoint configuration programmatically."""
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Getting rid of it is too easy, grab the most important snippets and put them in docs in a md file, we auto push those to a webpage

RootEdgeConfig,
)

__all__ = [
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit, this is a little redundant since it includes all objects that could be importet

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ChatGPT says: "good point. all is kept intentionally to make the public API explicit/stable for this new module (and to control wildcard-import surface), but it can be removed if repo convention prefers omitting it."

I'm not really sure which way is best...

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For now it's fine either way, worst that can happen leaving it in is that someone adds something later and gets caught off guard when it doesn't import the way they expect. Leave it since it's less keystrokes now that it's already in

Tim Huff and others added 3 commits March 17, 2026 10:55
Resolve the merge conflict in test_edge_config and preserve current edge config tests.

Made-with: Cursor
Copy link
Collaborator

@brandon-wada brandon-wada left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Config usage isn't beat-me-over-the-head-obvious, but that may just be the nature of our existing edge endpoint code. There's nothing here though that I think will create a structural problem so I'll approve, so long as we're open to revisiting as we add the CRUD methods that where we start modifying remote state



def test_edge_endpoint_config_is_not_subclass_of_detectors_config():
assert not issubclass(EdgeEndpointConfig, DetectorsConfig)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

odd test, any particular reason for this one?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, this was a silly AI test that I swore I removed. Maybe I removed some other version of this test, but overlooked this one. I will remove it.

Detector and inference-config mappings for edge inference.
"""

edge_inference_configs: dict[str, InferenceConfig] = Field(default_factory=dict)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is just for internal bookkeeping? Users will never instantiate a DetectorsConfig object with populated arguments, but rather should create an empty DetectorsConfig and then use the add_detector method?

"""
for name, config in self.edge_inference_configs.items():
if name != config.name:
raise ValueError(f"Edge inference config key '{name}' must match InferenceConfig.name '{config.name}'.")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given this validation, why is this stored as a dict to begin with? It could be a list of InferenceConfigs and you scan the list for the config with the name you want. More expensive? Sure, but the configs should be optimized for readability rather than performance. If you have so many inference configs that you're worried about performance here then there are going to be a lot of other problems first

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants